package com.moresir.validatetools;


import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import com.moresir.validatetools.common.response.ResponseEnum;
import com.moresir.validatetools.exceptions.BusinessException;
import com.moresir.validatetools.config.ReplaceConstantEnum;
import com.moresir.validatetools.exceptions.LexerExceptionEnum;
import com.moresir.validatetools.impl.Form;
import com.moresir.validatetools.impl.Rule;
import com.moresir.validatetools.impl.RulesMessages;
import com.moresir.validatetools.parser.RuleLexer;
import com.moresir.validatetools.parser.RuleParser;
import com.moresir.validatetools.util.ConstructRule;
import com.moresir.validatetools.util.ValidationData;
import org.apache.commons.lang3.StringUtils;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static com.moresir.validatetools.config.ValidatorConfig.cfg;

public class Validator {


    private Form form;
    private Map<String, List<Rule>> rules = new HashMap<>();
    private Map<String, String> initialRules = new HashMap<>();
    private Map<String, String> errors = new HashMap<>();
    private boolean hasValidate = false;
    private RulesMessages messages;
    private final String placeParamPattern = "\\{#([a-zA-Z_]+)#\\}";

    public Validator() {
        setLang(cfg().defaultLang);
    }

    public Validator(Form form) {
        this();
        this.form = form;
    }

    public void setForm(Form form) {
        this.errors.clear();
        this.hasValidate = false;
        this.form = form;
    }

    public JSONObject getJSONObjectByForm(){
        return JSONObject.parseObject(JSON.toJSONString(form.getMap()));
    }

    public void setLang(String lang) {
        messages = cfg().languageList.getByLanguage(lang);
    }

    /**
     * @param rule a rule syntax is: field... -> name_rule[:param...][|second_name_rule[:param...]]+ <br>
     *             <pre>
     *             samples of rules: <br>
     *                password -> confirm | min_length:5 <br>
     *                email    -> unique:user, email| email <br>
     *
     *             - All rules are lexed by {@link RuleLexer#lex()} <br>
     *             - All rules are transformed  by {@link RuleParser#getRuleInfo(String, String)}<br>
     *             </pre>
     * @param replacements is optional. if a rule contain array_contain:(:1) and replacement
     *                     a all :1 will be replaced by the first replacement.
     *                     If replacement is a $NUMBER, replacement will be executed after
     *                     the lexer.
     * @return this validator in order to chain {@link Validator#addRule(String, String, String...)}
     */
    public Validator addRule(String rule, String... replacements) {
        for (int i = 0; i < replacements.length; i++){
            rule = rule.replace(":" + (i+1), replacements[i]);
        }
        String[] exploded = rule.split(" -> ");
        if (exploded.length !=  2)
            throw new BusinessException(ResponseEnum.VALIDATE_RULE_LEXER_ERROR.getCode(), LexerExceptionEnum.SYNTAX_ERROR.getZhMsg());
        List<String> fields = Arrays.stream(exploded[0].split(","))
                .map(StringUtils::deleteWhitespace)
                .collect(Collectors.toList());
        String newRule = exploded[1];
        initialRules.put(exploded[0], exploded[1]);

        //newRule 中的占位符替换
        Pattern pattern = Pattern.compile(placeParamPattern);
        Matcher matcher = pattern.matcher(newRule);
        while (matcher.find()){
            String placePattern = matcher.group(1);
            String placePatternVal = ReplaceConstantEnum.getInPlacePatternValue(placePattern);
            newRule = newRule.replace(matcher.group(), placePatternVal);
        }

        //处理通配符
        List<String> newFields = new ArrayList<>();
        for(String tmpField: fields){
            if(tmpField.contains("*")){
                newFields = replaceAsterisksInField(newFields, tmpField);
                newFields.remove(tmpField);
            } else {
                newFields.add(tmpField);
            }
        }

        for (String field : newFields)
            addRule(field, newRule, replacements);
        return this;
    }

    private void addRule(String field, String rules, String... replacements) {
        List<RuleInfo> ruleInfos = RuleParser.getRuleInfo(field, rules);
        for (RuleInfo ruleInfo : ruleInfos) {
            ruleInfo.replacer(replacements);
            Optional<Rule> rule = ConstructRule.constructRule(ruleInfo.getRuleObj());
            if (!rule.isPresent())
                throw new BusinessException(ResponseEnum.VALIDATE_RULE_LEXER_ERROR.getCode(), String.format(LexerExceptionEnum.CONSTRUCT_RULE_ERROR.getZhMsg(), ruleInfo.getRuleName()));
            rule.get().injectRuleInfo(ruleInfo);
            this.rules.computeIfAbsent(field, k -> new ArrayList<>());
            this.rules.get(field).add(rule.get());
        }
    }

    public Map<String, String> check() {
        hasValidate = true;
        for(Map.Entry<String, List<Rule>> entry : rules.entrySet()){
            List<Rule> value = entry.getValue();
            String field = entry.getKey();

            boolean optional = value.stream().anyMatch(e -> e.getRuleInfo().isOptional());
            if ((!form.getString(field).isPresent() || form.getString(field).isPresent() &&
                    form.getString(field).get().isEmpty()) && !optional) {
                errors.put(field, field + " undefine or not exist or empty");
                break;
            }
            else if (!form.getString(field).isPresent() || form.getString(field).get().isEmpty() && optional)
                continue;
            for (Rule rule : value) {
                if (!rule.isOkay(form)) {
                    addError(field, rule.getRuleInfo());
                    break;
                }
            }
        }
        return getErrors();
    }

    private void addError(String field, RuleInfo ruleInfo) {
        Optional<String> msg = messages.getMessageFor(ruleInfo.getRuleName(), ruleInfo);
        msg.ifPresent(s -> errors.put(field, s));
    }

    public Form getForm() {
        return form;
    }

    public Map<String, List<Rule>> getRules() {
        return rules;
    }

    public void setRules(Map<String, List<Rule>> rules) {
        this.rules = rules;
    }

    public Map<String, String> getErrors() {
        return errors;
    }

    public void setErrors(Map<String, String> errors) {
        this.errors = errors;
    }

    public RulesMessages getMessages() {
        return messages;
    }

    public void setMessages(RulesMessages messages) {
        this.messages = messages;
    }

    /**
     * 替换 field 中的通配符：*
     */
    public List<String> replaceAsterisksInField(List<String> fields, String field){

        String[] asterisksFieldList = field.split("\\*", 2);
        String leadingExplicitAttributePath = StringUtils.strip(asterisksFieldList[0], ".");
        JSONArray leadingExplicitAttributeJSONArray = ValidationData.extractDataFromBeforeAsterisksPath(leadingExplicitAttributePath, getJSONObjectByForm());

        if(Objects.isNull(leadingExplicitAttributeJSONArray)){
            return fields;
        }

        for(int i = 0; i < leadingExplicitAttributeJSONArray.size(); i++){
            String newField = leadingExplicitAttributePath + "." + i + asterisksFieldList[1];
            if(newField.indexOf("*") > 0){
                fields.addAll(replaceAsterisksInField(Lists.newArrayList(), newField));
            } else {
                fields.add(newField);
            }
        }

        return fields;
    }

    public static String escapeExprSpecialWord(String keyword) {
        if (StringUtils.isNotBlank(keyword)) {
            String[] fbsArr = { "\\", "$", "(", ")", "*", "+", ".", "[", "]", "?", "^", "{", "}", "|" };
            for (String key : fbsArr) {
                if (keyword.contains(key)) {
                    keyword = keyword.replace(key, "\\" + key);
                }
            }
        }
        return keyword;
    }
}
